1 /**
2    The different TestCase classes
3  */
4 module unit_threaded.testcase;
5 
6 private shared(bool) _stacktrace = false;
7 
8 private void setStackTrace(bool value) @trusted nothrow @nogc {
9     synchronized {
10         _stacktrace = value;
11     }
12 }
13 
14 /// Let AssertError(s) propagate and thus dump a stacktrace.
15 public void enableStackTrace() @safe nothrow @nogc {
16     setStackTrace(true);
17 }
18 
19 /// (Default behavior) Catch AssertError(s) and thus allow all tests to be ran.
20 public void disableStackTrace() @safe nothrow @nogc {
21     setStackTrace(false);
22 }
23 
24 /**
25  * Class from which other test cases derive
26  */
27 class TestCase {
28 
29     import unit_threaded.io : Output;
30 
31     /**
32      * Returns: the name of the test
33      */
34     string getPath() const pure nothrow {
35         return this.classinfo.name;
36     }
37 
38     /**
39      * Executes the test.
40      * Returns: array of failures (child classes may have more than 1)
41      */
42     string[] opCall() {
43         static if (__VERSION__ >= 2077)
44             import std.datetime.stopwatch : StopWatch, AutoStart;
45         else
46             import std.datetime : StopWatch, AutoStart;
47 
48         currentTest = this;
49         auto sw = StopWatch(AutoStart.yes);
50         doTest();
51         flushOutput();
52         return _failed ? [getPath()] : [];
53     }
54 
55     /**
56      Certain child classes override this
57      */
58     ulong numTestsRun() const {
59         return 1;
60     }
61 
62     void showChrono() @safe pure nothrow {
63         _showChrono = true;
64     }
65 
66     void setOutput(Output output) @safe pure nothrow {
67         _output = output;
68     }
69 
70 package:
71 
72     static TestCase currentTest;
73     Output _output;
74 
75     void silence() @safe pure nothrow {
76         _silent = true;
77     }
78 
79     final Output getWriter() @safe {
80         import unit_threaded.io : WriterThread;
81 
82         return _output is null ? WriterThread.get : _output;
83     }
84 
85 protected:
86 
87     abstract void test();
88     void setup() {
89     } ///override to run before test()
90     void shutdown() {
91     } ///override to run after test()
92 
93 private:
94 
95     bool _failed;
96     bool _silent;
97     bool _showChrono;
98 
99     final auto doTest() {
100         import std.conv : text;
101         import std.datetime : Duration;
102 
103         static if (__VERSION__ >= 2077)
104             import std.datetime.stopwatch : StopWatch, AutoStart;
105         else
106             import std.datetime : StopWatch, AutoStart;
107 
108         auto sw = StopWatch(AutoStart.yes);
109         print(getPath() ~ ":\n");
110         check(setup());
111         check(test());
112         check(shutdown());
113         if (_failed)
114             print("\n");
115         if (_showChrono)
116             print(text("    (", cast(Duration) sw.peek, ")\n\n"));
117         if (_failed)
118             print("\n");
119     }
120 
121     final bool check(E)(lazy E expression) {
122         import unit_threaded.should : UnitTestException;
123 
124         try {
125             expression();
126         } catch (UnitTestException ex) {
127             fail(ex.toString());
128         } catch (Throwable ex) {
129             fail("\n    " ~ ex.toString() ~ "\n");
130         }
131 
132         return !_failed;
133     }
134 
135     final void fail(in string msg) {
136         _failed = true;
137         print(msg);
138     }
139 
140     final void print(in string msg) {
141         import unit_threaded.io : write;
142 
143         if (!_silent)
144             getWriter.write(msg);
145     }
146 
147     final void flushOutput() {
148         getWriter.flush;
149     }
150 }
151 
152 /**
153    A test that runs other tests.
154  */
155 class CompositeTestCase : TestCase {
156     void add(TestCase t) {
157         _tests ~= t;
158     }
159 
160     void opOpAssign(string op : "~")(TestCase t) {
161         add(t);
162     }
163 
164     override string[] opCall() {
165         import std.algorithm : map, reduce;
166 
167         return _tests.map!(a => a()).reduce!((a, b) => a ~ b);
168     }
169 
170     override void test() {
171         assert(false, "CompositeTestCase.test should never be called");
172     }
173 
174     override ulong numTestsRun() const {
175         return _tests.length;
176     }
177 
178     package TestCase[] tests() @safe pure nothrow {
179         return _tests;
180     }
181 
182     override void showChrono() {
183         foreach (test; _tests)
184             test.showChrono;
185     }
186 
187 private:
188 
189     TestCase[] _tests;
190 }
191 
192 /**
193    A test that should fail
194  */
195 class ShouldFailTestCase : TestCase {
196     this(TestCase testCase, in TypeInfo exceptionTypeInfo) {
197         this.testCase = testCase;
198         this.exceptionTypeInfo = exceptionTypeInfo;
199     }
200 
201     override string getPath() const pure nothrow {
202         return this.testCase.getPath;
203     }
204 
205     override void test() {
206         import unit_threaded.should : UnitTestException;
207         import std.exception : enforce, collectException;
208         import std.conv : text;
209 
210         const ex = collectException!Throwable(testCase.test());
211         enforce!UnitTestException(ex !is null,
212                 "Test '" ~ testCase.getPath ~ "' was expected to fail but did not");
213         enforce!UnitTestException(exceptionTypeInfo is null
214                 || typeid(ex) == exceptionTypeInfo, text("Test '", testCase.getPath,
215                     "' was expected to throw ", exceptionTypeInfo, " but threw ", typeid(ex)));
216     }
217 
218 private:
219 
220     TestCase testCase;
221     const(TypeInfo) exceptionTypeInfo;
222 }
223 
224 /**
225    A test that is a regular function.
226  */
227 class FunctionTestCase : TestCase {
228 
229     import unit_threaded.reflection : TestData, TestFunction;
230 
231     this(in TestData data) pure nothrow {
232         _name = data.getPath;
233         _func = data.testFunction;
234     }
235 
236     override void test() {
237         _func();
238     }
239 
240     override string getPath() const pure nothrow {
241         return _name;
242     }
243 
244     private string _name;
245     private TestFunction _func;
246 }
247 
248 /**
249    A test that is a `unittest` block.
250  */
251 class BuiltinTestCase : FunctionTestCase {
252 
253     import unit_threaded.reflection : TestData;
254 
255     this(in TestData data) pure nothrow {
256         super(data);
257     }
258 
259     override void test() {
260         import core.exception : AssertError;
261 
262         try
263             super.test();
264         catch (AssertError e) {
265             import unit_threaded.should : fail;
266 
267             fail(_stacktrace ? e.toString() : e.msg, e.file, e.line);
268         }
269     }
270 }
271 
272 /**
273    A test that is expected to fail some of the time.
274  */
275 class FlakyTestCase : TestCase {
276     this(TestCase testCase, int retries) {
277         this.testCase = testCase;
278         this.retries = retries;
279     }
280 
281     override string getPath() const pure nothrow {
282         return this.testCase.getPath;
283     }
284 
285     override void test() {
286 
287         foreach (i; 0 .. retries) {
288             try {
289                 testCase.test;
290                 break;
291             } catch (Throwable t) {
292                 if (i == retries - 1)
293                     throw t;
294             }
295         }
296     }
297 
298 private:
299 
300     TestCase testCase;
301     int retries;
302 }